package com.atguigu.aops;

import com.atguigu.annotations.MyRedisCache;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

/**
 * 自定义Redis缓存切面类，用于实现基于注解的Redis缓存功能
 * 通过AOP切面编程，在方法执行前后进行Redis缓存的读取和存储操作
 */
@Slf4j
@Aspect
@Component
public class MyRedisCacheAspect {
    /**
     * 注入RedisTemplate实例，用于执行Redis的各种操作
     * RedisTemplate是Spring Data Redis提供的操作Redis的核心类
     */
    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 定义一个切点，用于拦截带有@MyRedisCache注解的方法
     * 该切点使用AspectJ的@Pointcut注解，匹配所有被@MyRedisCache注解标记的方法
     * 返回值: 无
     */
    @Pointcut("@annotation(com.atguigu.annotations.MyRedisCache)")
    public void pc(){};

    /**
     * 环绕通知方法，用于实现Redis缓存拦截逻辑
     * 该方法会在目标方法执行前后进行拦截，首先尝试从Redis缓存中获取数据，
     * 如果缓存中存在则直接返回，否则执行目标方法并将结果存入Redis缓存
     *
     * @param joinPoint 连接点对象，包含目标方法的信息和执行上下文
     * @return 目标方法的执行结果或缓存中的数据
     */
    @Around("pc()")
    public Object around(ProceedingJoinPoint joinPoint) {
        // 定义返回结果变量，初始化为null
        Object result = null;
        try {
            // 获取目标方法的签名信息，通过签名可以获取方法的各种元数据
            // MethodSignature是Signature接口的子接口，专门用于方法签名
            MethodSignature signature =(MethodSignature) joinPoint.getSignature();

            // 通过方法签名获取具体的方法对象，用于后续获取注解和参数信息
            // getMethod()返回的是目标类中声明的方法对象
            Method method = signature.getMethod();

            // 获取方法上的自定义缓存注解@MyRedisCache
            // getAnnotation()方法用于获取指定类型的注解实例
            MyRedisCache myRedisCache = method.getAnnotation(MyRedisCache.class);

            // 从注解中获取缓存key的前缀部分，用于构造完整的Redis key
            String keyPrefix = myRedisCache.keyPrefix();

            // 从注解中获取匹配值表达式，通常是SpEL表达式，用于动态生成key的后缀部分
            String matchValue = myRedisCache.matchValue();

            // 创建SpEL表达式解析器，用于解析Spring Expression Language表达式
            // SpelExpressionParser是Spring提供的表达式解析器实现
            SpelExpressionParser spelExpressionParser=new SpelExpressionParser();

            // 使用解析器解析matchValue表达式字符串，返回Expression对象
            // parseExpression()方法将字符串表达式解析为可执行的表达式对象
            Expression expression = spelExpressionParser.parseExpression(matchValue);

            // 创建标准的表达式求值上下文，用于在表达式求值时提供变量和函数
            // StandardEvaluationContext提供了完整的SpEL上下文实现
            EvaluationContext context = new StandardEvaluationContext();

            // 获取目标方法的参数值数组，用于后续设置到表达式上下文中
            // getArgs()返回连接点方法调用时传入的实际参数值
            Object[] args = joinPoint.getArgs();

            // 创建参数名发现器，用于获取方法参数的名称
            // DefaultParameterNameDiscoverer是Spring提供的默认参数名发现器实现
            DefaultParameterNameDiscoverer discoverer = new DefaultParameterNameDiscoverer();

            // 通过参数名发现器获取方法参数的名称数组
            // getParameterNames()方法根据Method对象获取参数名称
            String[] parameterNames = discoverer.getParameterNames(method);

            // 遍历所有参数，将参数名和参数值设置到表达式上下文中
            // 这样在解析SpEL表达式时就可以通过参数名引用参数值
            for (int i = 0; i < parameterNames.length; i++) {
                System.out.println(parameterNames[i]+":"+args[i]);
                // setVariable()方法在上下文中设置命名变量，供表达式使用
                context.setVariable(parameterNames[i], args[i]);
            }

            // 构造完整的Redis缓存key，由前缀和表达式求值结果组成
            // getValue()方法在指定上下文中对表达式进行求值，返回结果
            String key = keyPrefix + ":" + expression.getValue(context);

            System.out.println("------redis 查询 key: " + key);

            // 使用RedisTemplate的操作对象opsForValue()执行get操作，从Redis中获取数据
            // opsForValue()返回ValueOperations对象，用于操作Redis的字符串类型数据
            result = redisTemplate.opsForValue().get(key);

            // 判断Redis中是否查询到数据
            if (result != null) {
                System.out.println("------redis里面有，我直接返回结果不再打扰mysql: " + result);
                // 如果缓存中有数据，直接返回，不再执行目标方法（避免查询数据库）
                return result;
            }

            // 如果Redis缓存中没有数据，则执行目标方法（通常会查询数据库）
            // proceed()方法执行连接点的原始方法，返回方法执行结果
            result = joinPoint.proceed();

            // 判断目标方法执行后是否有返回结果
            if(result!= null){
                // 将方法执行结果存入Redis缓存，使用之前构造的key
                // set()方法将指定的键值对存储到Redis中
                redisTemplate.opsForValue().set(key, result);
                System.out.println("------redis里面没有，我直接返回结果并保存到redis: " + result);
                // 返回目标方法的执行结果
                return result;
            }
        } catch (Throwable e) {
            // 捕获所有可能的异常，避免因缓存操作异常影响主业务流程
            e.printStackTrace();
        }
        // 如果出现异常或没有查询到结果，返回默认的null值
        return result;
    }
}
